/*	$Id: conf.y 1123 2010-12-16 04:03:27Z phrakt $	*/
/*
 * Copyright (c) 2004 Jean-Francois Brousseau <jfb@openbsd.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

%{
#include <sys/param.h>

#include <errno.h>
#include <ctype.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <string.h>

#include <canopy/mem.h>
#include <canopy/queue.h>
#include <canopy/string.h>

#include "buf.h"
#include "log.h"
#include "conf.h"
#include "htab.h"
#include "private.h"




/* parser states */
enum cnp_conf_state {
	CFP_STATE_INIT,
	CFP_STATE_PARSING,
	CFP_STATE_DONE,
	CFP_STATE_ERROR,
};


#define CFP_SETERR(p, e)				\
	do {						\
		(p)->cf_state = CFP_STATE_ERROR;	\
		(p)->cf_error = (e);			\
	} while (0)


/*
 * A configuration input source, either a file or a string.  In the case of a
 * file, <cf_path> points to the file path, <cf_fin> is a FILE handle to that
 * file, <cf_lineno> stores the current line number, and <cf_line> is NULL.
 * In the case of a single line, <cf_line> points to that line, and all other
 * fields are unused.
 *
 */
struct cnp_conf_input {
	const char     *cf_path;
	const char     *cf_line;
	FILE           *cf_fin;
	int             cf_lineno;
};


/* configuration directive information */
struct cnp_conf_dir {
	char  *cd_name;
	int    cd_minargc;
	int    cd_maxargc;
	int  (*cd_hdlr)(int, char **);

	TAILQ_ENTRY(cnp_conf_dir) cd_link;
};



struct cnp_conf_ctx {
	int                       cf_flags;
	enum cnp_conf_state       cf_state;
	int                       cf_error;		/* error code, if any */

	struct cnp_conf_input    *cf_input[CNP_CONF_MAXINPUT];
	unsigned int              cf_incount;
	cnp_buf_t                 *cf_buf;		/* pushback buffer */

	cnp_htab_t                *cf_vars;

	TAILQ_HEAD(, cnp_conf_dir) cf_dirs;	/* directives */

	/* argument vector for directives */
	char   *cf_argv[CONF_DIR_MAXARG];
	int     cf_argc;
};




struct yystype {
	union {
		uint64_t              num;
		char                 *str;
		uint8_t              *bstr;
		struct cnp_conf_dir  *dir;
	} v;

	size_t        vlen;
	unsigned int  type;
	int           lineno;
};

#define YYSTYPE struct yystype



/* reserved characters */
static const char conf_reserved[] = "=${}[]";


/*
 * In order to have a reentrant parser, we have to use a big lock model
 */
static cnp_conf_t *cnp_conf_current = NULL;	/* pointer to current parser */

static cnp_mutex_t cnp_conf_lock = CNP_MUTEX_INITIALIZER;



static int                  cnp_conf_getc       (cnp_conf_t *);
static int                  cnp_conf_ungetc     (cnp_conf_t *, int);
static void                 cnp_conf_log        (enum cnp_log_pri, cnp_conf_t *,
							const char *, ...);
static struct cnp_conf_dir* cnp_conf_finddir    (cnp_conf_t *, const char *);
static int                  cnp_conf_rundir     (cnp_conf_t *);
static int                  cnp_conf_chksymname (const char *);


static int  yyerror     (const char *, ...);
static int  yylex       (void);
static int  yylex_var   (void);
#if 0
static int  yylex_qstr  (int);
#endif
static int  yyparse     (void);


%}

%token  <v.str>   STRING
%token  <v.num>   NUMBER
%token  <v.dir>   DIRECTIVE
%%

conf		: /* empty */
		| conf '\n'
		| conf var_assign '\n'
		| conf DIRECTIVE
		{
			cnp_conf_log(CNP_LOG_DEBUG, cnp_conf_current,
			    "found directive `%s'", $2->cd_name);
			cnp_conf_current->cf_argv[0] = $2->cd_name;
			cnp_conf_current->cf_argc = 1;
		} dir_argv '\n'
		{
			if (cnp_conf_rundir(cnp_conf_current) == -1) {
				YYERROR;
			}
		}
		| conf error '\n'
		{
			YYERROR;
			CFP_SETERR(cnp_conf_current, CONF_ERR_PARSE);
		}
		;

var_assign	: STRING '=' STRING
		{
			cnp_conf_setvar(cnp_conf_current, $1, $3);
			free($1);
			free($3);
		}
		;


dir_argv	: /* empty */
		| dir_argv STRING
		{
			if (cnp_conf_current->cf_argc == CONF_DIR_MAXARG) {
				YYERROR;
			}

			cnp_conf_current->cf_argv[cnp_conf_current->cf_argc++] = $2;
		}

%%


/*
 * cnp_conf_getc()
 *
 * Retrieve the next available character from the stream being parsed by
 * the configuration parser <conf>.
 */
static int
cnp_conf_getc(cnp_conf_t *conf)
{
	int c;
	struct cnp_conf_input *input = conf->cf_input[conf->cf_incount - 1];

	/* check if we've got something in the parse buffer first */
	if (cnp_buf_len(conf->cf_buf) > 0)
		c = cnp_buf_getc(conf->cf_buf);
	else if (input->cf_fin != NULL)
		c = getc(input->cf_fin);
	else {
		c = *(input->cf_line)++;
		if (c == '\0')
			c = EOF;
	}

	return (c);
}


/*
 * cnp_conf_ungetc()
 *
 * Push back the character <c> in the parser <conf>'s parse buffer.  We do not
 * rely on ungetc() because it guarantees only one character pushback and,
 * although this mostly what we need, some cases may arise where we need
 * more pushback guaranteed.
 */
static int
cnp_conf_ungetc(cnp_conf_t *conf, int c)
{
	return cnp_buf_ungetc(conf->cf_buf, c);
}


/*
 * cnp_conf_alloc()
 *
 * Allocate a new configuration parser and initialize its internals for the
 * parsing of files which have the format specified in <style>.
 * Returns a pointer to the new parser on success, or NULL on failure.
 */
cnp_conf_t*
cnp_conf_alloc(int flags)
{
	cnp_conf_t *conf;

	if ((conf = cnp_malloc(sizeof(*conf))) == NULL) {
		cnp_log_errno(cnp_logchan,
		    "failed to allocate configuration object");
		return (NULL);
	}
	memset(conf, 0, sizeof(*conf));

	conf->cf_flags = flags;

	TAILQ_INIT(&(conf->cf_dirs));

	if ((conf->cf_vars = cnp_htab_alloc(8, CNP_HTAB_COPYKEY, NULL,
	    NULL, cnp_free)) == NULL) {
		cnp_conf_free(conf);
		return (NULL);
	}

	if ((conf->cf_buf = cnp_buf_alloc(CONF_BUF_SIZE, 0)) == NULL) {
		cnp_log_errno(cnp_logchan,
		    "failed to allocate configuration parse buffer");
		cnp_conf_free(conf);
		return (NULL);
	}

	conf->cf_argc = 0;
	memset(conf->cf_argv, 0, sizeof(conf->cf_argv));

	return (conf);
}


/*
 * cnp_conf_free()
 *
 * Free all resources currently used by the parser <conf>.  Any further
 * operation on this parser will have undefined behaviour.
 */
void
cnp_conf_free(cnp_conf_t *conf)
{
	struct cnp_conf_dir *dir;

	if (conf != NULL) {
		if (conf->cf_buf != NULL)
			cnp_buf_free(conf->cf_buf);

		if (conf->cf_vars != NULL)
			cnp_htab_free(conf->cf_vars);

		while (!TAILQ_EMPTY(&conf->cf_dirs)) {
			dir = TAILQ_FIRST(&conf->cf_dirs);
			TAILQ_REMOVE(&conf->cf_dirs, dir, cd_link);
			cnp_free(dir->cd_name);
			cnp_free(dir);
		}

		cnp_free(conf);
	}
}

/*
 * cnp_conf_reset()
 *
 * Reset the parser's internal states.  Once this operation has been performed,
 * the parser should be ready to parser a new configuration file.
 */
void
cnp_conf_reset(cnp_conf_t *conf)
{
	conf->cf_state = CFP_STATE_INIT;
	conf->cf_error = CONF_ERR_NOERR;
}

/*
 * cnp_conf_getfl()
 *
 * Returns the configuration parser's flags.
 */
int
cnp_conf_getfl(cnp_conf_t *conf)
{
	return (conf->cf_flags);
}

/*
 * cnp_conf_setfl()
 *
 * Set the parser's flags to <flags>.
 */
int
cnp_conf_setfl(cnp_conf_t *conf, int flags)
{
	conf->cf_flags = flags;
	return (0);
}

/*
 * cnp_conf_parse()
 *
 * Parse the contents of the configuration file located at <path> according to
 * the configuration context <conf>.
 *
 * Returns 0 on success, or -1 on error.
 */
int
cnp_conf_parse(cnp_conf_t *conf, const char *path)
{
	int ret = 0;
	struct cnp_conf_input input;

	cnp_log_debug(cnp_logchan, "parsing configuration file %s", path);

	input.cf_path = path;
	input.cf_line = NULL;
	input.cf_lineno = 1;

	if ((input.cf_fin = fopen(path, "r")) == NULL) {
		cnp_log_errno(cnp_logchan, "failed to open %s", path);
		return (-1);
	}

	if (cnp_mutex_lock(&cnp_conf_lock) != 0) {
		cnp_log_err(cnp_logchan,
		    "failed to obtain global configuration lock");
		(void)fclose(input.cf_fin);
		return (-1);
	}

	conf->cf_input[conf->cf_incount++] = &input;

	/* set ourself as the current parser */
	cnp_conf_current = conf;

	cnp_log_debug(cnp_logchan, "calling yyparse()");
	if (yyparse() != 0)
		ret = -1;

	conf->cf_incount--;

	(void)cnp_mutex_unlock(&cnp_conf_lock);

	(void)fclose(input.cf_fin);

	return (ret);
}

/*
 * cnp_conf_parseln()
 *
 * Parse a single configuration line <line> with the configuration context
 * <conf>.
 *
 * Returns 0 on success, or -1 on failure.
 */
int
cnp_conf_parseln(cnp_conf_t *conf, const char *line)
{
	int ret = 0;
	struct cnp_conf_input input;

	input.cf_path = NULL;
	input.cf_line = line;
	input.cf_fin = NULL;

	if (cnp_mutex_lock(&cnp_conf_lock) != 0)
		return (-1);

	cnp_conf_log(CNP_LOG_DEBUG, conf, "parsing line `%s'", line);

	conf->cf_input[conf->cf_incount++] = &input;

	/* set ourself as the current parser */
	cnp_conf_current = conf;

	cnp_log_debug(cnp_logchan, "calling yyparse()");
	if (yyparse() != 0)
		ret = -1;

	conf->cf_incount--;

	(void)cnp_mutex_unlock(&cnp_conf_lock);

	return (ret);
}

/*
 * cnp_conf_getvar()
 *
 * Get a macro <macro>'s associated value.  Returns the value string on
 * success, or NULL if no such macro exists.
 */
int
cnp_conf_getvar(cnp_conf_t *conf, const char *name, char *dst, size_t len)
{
	int ret = -1;
	char *val;

	if ((val = cnp_htab_strlookup(conf->cf_vars, name)) != NULL) {
		ret = 0;
		strlcpy(dst, val, len);
	}

	return (ret);
}

/*
 * cnp_conf_getvars()
 *
 * Returns a reference to a hash table containing all the variables defined
 * in the configuration <conf>.  The reference must be freed by the caller
 * once it is no longer used.
 */
cnp_htab_t*
cnp_conf_getvars(cnp_conf_t *conf)
{
	return cnp_htab_ref(conf->cf_vars);

}

/*
 * cnp_conf_setvar()
 *
 * Set the value associated with the variable whose name is specified in
 * <name> to the string <val> for the configuration <conf>.
 *
 * Returns 0 on success, or -1 on failure.
 */
int
cnp_conf_setvar(cnp_conf_t *conf, const char *name, const char *val)
{
	int ret;
	char *copy;

	/* validate arguments */
	if (!cnp_conf_chksymname(name)) {
		cnp_conf_log(CNP_LOG_ERR, conf, "invalid variable name `%s'",
		    name);
		CFP_SETERR(conf, CONF_ERR_INVARG);
		return (-1);
	}

	if ((copy = cnp_strdup(val)) == NULL) {
		return (-1);
	}

	cnp_conf_log(CNP_LOG_DEBUG, conf, "setting %s to `%s'", name, val);
	ret = cnp_htab_strinsert(conf->cf_vars, name, copy);
	if (ret == -1) {
		cnp_free(copy);
	}

	return (ret);
}

/*
 * cnp_conf_chksymname()
 *
 * Validate the proposed symbol (variable or directive) name <name>.
 *
 * Returns 1 if the name is OK, 0 if it doesn't conform to the variable
 * naming convention.
 */
static int
cnp_conf_chksymname(const char *name)
{
	const char *np;

	if (!isalpha(*name) && (*name != '_'))
		return (0);

	for (np = name + 1; *np != '\0'; np++)
		if (!isalnum(*name) && (*name != '_'))
			return (0);

	return (1);
}

/*
 * cnp_conf_setdir()
 *
 */
int
cnp_conf_setdir(cnp_conf_t *conf, const char *name, int (*hdlr)(int, char **),
    int min, int max)
{
	char *tmp;
	struct cnp_conf_dir *cdp;

	/* check the validity of the directive name */
	if (!cnp_conf_chksymname(name)) {
		cnp_conf_log(CNP_LOG_ERR, conf, "invalid directive name `%s'",
		    name);
		CFP_SETERR(conf, CONF_ERR_INVARG);
		return (-1);
	}

	if ((tmp = cnp_strdup(name)) == NULL) {
		cnp_log_err(cnp_logchan,
		    "failed to allocate configuration directive");
		return (-1);
	}

	if ((cdp = cnp_conf_finddir(conf, name)) != NULL) {
		cnp_free(cdp->cd_name);
	}
	else if ((cdp = cnp_malloc(sizeof(*cdp))) == NULL) {
		cnp_log_err(cnp_logchan,
		    "failed to allocate configuration directive");
		cnp_free(tmp);
		return (-1);
	}
	else {
		TAILQ_INSERT_TAIL(&(conf->cf_dirs), cdp, cd_link);
	}

	cdp->cd_name = tmp;
	cdp->cd_hdlr = hdlr;
	cdp->cd_minargc = min;
	cdp->cd_maxargc = max;

	return (0);
}

/*
 * cnp_conf_finddir()
 *
 * Look for the directive whose name is <name> in the context of the parser
 * <conf> and return a pointer to its structure if found, or NULL otherwise.
 */
static struct cnp_conf_dir*
cnp_conf_finddir(cnp_conf_t *conf, const char *name)
{
	struct cnp_conf_dir *cdp;

	cnp_conf_log(CNP_LOG_DEBUG, conf, "cnp_conf_finddir(%s)", name);

	TAILQ_FOREACH(cdp, &(conf->cf_dirs), cd_link)
		if (strcmp(cdp->cd_name, name) == 0)
			return (cdp);

	return (NULL);
}

/*
 * cnp_conf_rundir()
 *
 * Run the directive specified in the first entry of the directive argument
 * vector, along with the other arguments supplied.  After the directive was
 * run, the argument vector is reinitialized to prepare for the next call.
 * Returns the directive's return value on success, or -1 on failure.
 */
static int
cnp_conf_rundir(cnp_conf_t *conf)
{
	int i, ret;
	struct cnp_conf_dir *cd;

	cd = cnp_conf_finddir(conf, conf->cf_argv[0]);
	if (cd == NULL) {
		cnp_conf_log(CNP_LOG_ERR, conf, "directive `%s' not found",
		    conf->cf_argv[0]);
		ret = -1;
	}
	else if ((cd->cd_minargc > 0) && (conf->cf_argc < cd->cd_minargc)) {
		cnp_conf_log(CNP_LOG_ERR, conf,
		    "not enough arguments to `%s' directive", cd->cd_name);
		ret = -1;
	}
	else if ((cd->cd_maxargc >= 0) && (conf->cf_argc > cd->cd_maxargc)) {
		cnp_conf_log(CNP_LOG_ERR, conf,
		    "too many arguments to `%s' directive", cd->cd_name);
		ret = -1;
	}
	else {
		cnp_conf_log(CNP_LOG_DEBUG, conf, "running directive `%s'",
		    cd->cd_name);
		ret = (*cd->cd_hdlr)(conf->cf_argc, conf->cf_argv);
		if (ret < 0)
			cnp_conf_log(CNP_LOG_WARN, conf,
			    "directive `%s' returned %d", cd->cd_name, ret);
	}

	/* reset the argument vector */
	for (i = 1; i < conf->cf_argc; i++) {
		cnp_free(conf->cf_argv[i]);
		conf->cf_argv[i] = NULL;
	}

	conf->cf_argc = 0;

	return (ret);
}

/*
 * cnp_conf_log()
 *
 */
static void
cnp_conf_log(enum cnp_log_pri level, cnp_conf_t *conf, const char *fmt, ...)
{
	char *nfmt;
	va_list vap;
	struct cnp_conf_input *input = NULL;

	if (conf->cf_incount > 0)
		input = conf->cf_input[conf->cf_incount - 1];

	va_start(vap, fmt);

	if ((input != NULL) && (input->cf_path != NULL)) {
		cnp_asprintf(&nfmt, "%s[%u]: %s", input->cf_path,
		    input->cf_lineno, fmt);
		cnp_vlog(cnp_logchan, level, nfmt, vap);
		free(nfmt);
	}
	else
		cnp_vlog(cnp_logchan, level, fmt, vap);

	va_end(vap);
}

/*
 * yylex()
 *
 * Our own lexer.
 */
static int
yylex(void)
{
	int c;
	char buf[CONF_TOK_MAXLEN], *bp, *ep;
	cnp_conf_t *conf = cnp_conf_current;
	struct cnp_conf_input *input;

	input = conf->cf_input[conf->cf_incount - 1];

	buf[0] = '\0';
	buf[1] = '\0';

	bp = buf;
	ep = buf + sizeof(buf) - 1;

	yylval.lineno = input->cf_lineno;

	/* skip whitespace */
	while (((c = cnp_conf_getc(conf)) == ' ') || (c == '\t'))
		;

	if (c == '#') {
		/* strip comment up to end of line */
		do {
			c = cnp_conf_getc(conf);
		} while ((c != '\n') && (c != EOF));
	}

	if (c == EOF)
		c = 0;
	else if (c == '\n')
		input->cf_lineno++;
	else if ((c == '=') || (c == '[') || (c == ']')) {
		buf[0] = c;		/* nothing */
	} else if ((c == '"') || (c == '\'')) {
	}
	else {
		/* string */
		do {
			if (c == '$') {
				if (yylex_var() == -1)
					return (-1);

				c = cnp_conf_getc(conf);
			}

			*bp++ = (char)c;
			if (bp == ep) {
				CFP_SETERR(conf, CONF_ERR_TOKLEN);
				return (-1);
			}

			c = cnp_conf_getc(conf);
		} while ((c != EOF) && !isblank(c) && (c != '\n') &&
		    (c != '='));
		cnp_conf_ungetc(conf, c);
		*bp = '\0';

		if ((yylval.v.dir = cnp_conf_finddir(conf, buf)) != NULL)
			c = DIRECTIVE;
		else {
			c = STRING;
			if ((yylval.v.str = cnp_strdup(buf)) == NULL)
				return (-1);
		}
	}

	cnp_conf_log(CNP_LOG_DEBUG, conf, "yylex: token[%d] = `%s'", c, buf);

	return (c);
}

/*
 * yylex_var()
 *
 */
static int
yylex_var(void)
{
	int c, brkt;
	char varbuf[128], *bp, *val;
	cnp_conf_t *conf = cnp_conf_current;

	bp = varbuf;

	c = cnp_conf_getc(conf);
	brkt = (c == '{') ? 1 : 0;
	if (brkt)
		c = cnp_conf_getc(conf);

	do {
		if (brkt && (c == '}'))
			break;

		*bp++ = (char)c;
		if (bp == (varbuf + sizeof(varbuf))) {
			CFP_SETERR(conf, CONF_ERR_BADNAME);
			return (-1);
		}
		c = cnp_conf_getc(conf);
	} while (isalnum(c) || (c == '_') || (c == '.'));

	if (!brkt)
		cnp_conf_ungetc(conf, c);
	*bp = '\0';

	if ((val = cnp_htab_strlookup(conf->cf_vars, varbuf)) == NULL) {
		if (conf->cf_flags & CONF_STRICTVAR) {
			CFP_SETERR(conf, CONF_ERR_STRICT);
			return (-1);
		}
	}
	else {
		cnp_buf_append(conf->cf_buf, val, strlen(val));
	}

	return (0);
}

#if 0
/*
 * yylex_qstr()
 *
 * Take care of quoted strings.  The <quote> argument tells us what kind of
 * quote opened the string.
 */
static int
yylex_qstr(int quote)
{
	int c;
	char buf[10];
	char *bp, *ep;
	cnp_conf_t *conf = cnp_conf_current;

	while ((c = cnp_conf_getc(conf)) != quote) {
		if (quote == '"') {
			if (c == '\\') {
				c = cnp_conf_getc(conf);
				switch (c) {
				case 'n':
					c = '\n';
					break;
				case 'r':
					c = '\r';
					break;
				case 't':
					c = '\t';
					break;
				default:
					break;
				}
			}
		}

		*bp++ = (char)c;
		if (bp == ep) {
			CFP_SETERR(conf, CONF_ERR_TOKLEN);
			return (-1);
		}
	}
	*bp = '\0';
	if ((yylval.v.str = cnp_strdup(buf)) == NULL) {
		CFP_SETERR(conf, CONF_ERR_NOMEM);
		return (-1);
	}
	c = STRING;

	return (c);
}
#endif

/*
 * yyerror()
 *
 */
static int
yyerror(const char *fmt, ...)
{
	char msgbuf[128];
	va_list vap;

	va_start(vap, fmt);
	vsnprintf(msgbuf, sizeof(msgbuf), fmt, vap);
	va_end(vap);

	cnp_conf_log(CNP_LOG_ERR, cnp_conf_current, msgbuf);
	return (0);
}
